package se.webbzon.boltzmann.editor.view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.io.File;
import java.util.Collection;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import se.webbzon.boltzmann.editor.mvc.EditorListener;
import se.webbzon.boltzmann.editor.mvc.EditorModel;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldarea.WorldEngine;
import se.webbzon.oschi01.worldcamera.JWorldView;
import se.webbzon.oschi01.worldcamera.WorldCamera;
import se.webbzon.oschi01.worldcamera.WorldViewFactory;
import se.webbzon.oschi01.worldobject.WorldObject;
import se.webbzon.oschi01.worldobject.WorldStaticObject;

public class WorldViewContainer extends JPanel {
	
	// Auto generated serial version UID
	private static final long serialVersionUID = 8780924362705170506L;
	
	// The minimal camera radius
	private static final double minRadius = 0.01;
	
	// The factors used to determine the motion of the camera
	private static final double moveFactor = 0.002;
	private static final double zoomFactor = 0.05;
	private static final double spinFactor = 0.01;
	
	// The dimension of the container
	private final Dimension d;
	
	// The world engine associated with this container
	private WorldEngine engine;
	
	// The world camera associated with this container
	private WorldCamera camera;
	
	// The target of the camera
	private double tx, ty, tz;
	
	// The height, radius and angle of the camera
	private double r, phi, theta;
	
	/*============================================================
	Constructors
	============================================================*/
	
	public WorldViewContainer(EditorModel model) {
		d = new Dimension(800,600);
		
		model.addListener(new EditorListenerImpl());
		
		add(new PlaceHolder(d));
		MouseListenerImpl listener = new MouseListenerImpl();
		addMouseListener(listener);
		addMouseMotionListener(listener);
		addMouseWheelListener(listener);
		addKeyListener(new WorldViewKeyListener(model));
		setFocusable(true);
		requestFocus();
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Updates the content of the container. **/
	private void updateContainer() {
		removeAll();
		if (engine == null) {
			add(new PlaceHolder(d));
			camera = null;
		} else {
			JWorldView view = 
					new WorldViewFactory(	WorldViewFactory.TRANSPARENCY,
											engine).makeJView(d);
			camera = view.getCamera();
			camera.setProjectionMaxDistance(1024);
			setCamera(0,0,0,256,-Math.PI/2,Math.PI/4);
			moveCamera();
			add(view);
			requestFocus();
		}
	}
	
	/** Sets the default position of the camera. **/
	private void setCamera(	double tx, double ty, double tz, 
							double r, double phi, double theta) {
		this.tx = tx;
		this.ty = ty;
		this.tz = tz;
		this.theta = Math.min(Math.PI/2,Math.max(-Math.PI/2,theta));
		this.r = Math.max(0, r);
		this.phi = phi;
	}
	
	/** Moves the camera to the given position. **/
	private void moveCamera(double tx, double ty, double tz, 
							double r, double phi, double theta) {
		if (camera == null)
			return;
		
		final double rf = Math.max(minRadius, r);
		final double thetaf = Math.min(Math.PI/2,Math.max(-Math.PI/2,theta));
		final double cosTheta = Math.cos(thetaf);
		final Point3d to = new Point3d(tx,ty,tz);
		final Point3d from = new Point3d(	tx+rf*Math.cos(phi)*cosTheta,
											ty+rf*Math.sin(thetaf),
											tz-rf*Math.sin(phi)*cosTheta);
		
		camera.projectPerspective(from,to,new Vector3d(0,1,0));
	}
	
	/** Moves the camera to the default position. **/
	private void moveCamera() {
		moveCamera(tx,ty,tz,r,phi,theta);
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	/** A gray place holder for the WorldView. **/
	private static final class PlaceHolder extends JComponent {
		
		// Auto generated serial version UID
		private static final long serialVersionUID = -6072589635637341443L;

		/*============================================================
		Constructors
		============================================================*/
		
		/** Creates a new PlaceHolder. **/
		public PlaceHolder(Dimension d) {
			setPreferredSize(d);
		}
		
		/*============================================================
		Public Methods
		============================================================*/
		
		@Override public void paintComponent(Graphics g) {
			g.setColor(Color.lightGray);
			g.fillRect(0, 0, getWidth(), getHeight());
		}
		
	}
	
	/** An implementation of an editor listener. **/
	private final class EditorListenerImpl implements EditorListener {

		/*============================================================
		Public Methods
		============================================================*/
		
		@Override public void onOpenWorld(Object source, WorldArea world) {
			if (engine == null)
				engine = new WorldEngine(world);
			else
				engine.setWorld(world);
			for (WorldStaticObject instance : world.findInstances(WorldStaticObject.class))
				instance.refreshTransform(true, true);
			updateContainer();
		}
		
		@Override public void onCloseWorld(Object source, WorldArea world) {
			engine = null;
			updateContainer();
		}

		@Override public void onAddInstance(Object source, WorldObject instance) {
			if (instance instanceof WorldStaticObject)
				((WorldStaticObject) instance).refreshTransform(true, true);
		}

		@Override public void onRemoveInstance(Object source, WorldObject instance) {};

		@Override public void onSelectInstances(
				Object source,
				Collection<WorldObject> selectedInstances,
				Collection<WorldObject> deselectedInstances) {}

		@Override public void onSelectFile(Object source, File file) {}

		@Override public void onChangeInstance(Object source, WorldObject instance) {
			if (instance instanceof WorldStaticObject)
				((WorldStaticObject) instance).refreshTransform(true, true);
		}
		
		@Override public void onFocusInstance(Object source, WorldObject instance) {
			if (instance instanceof WorldStaticObject) {
				WorldStaticObject wso = (WorldStaticObject) instance;
				setCamera(wso.getX(),wso.getY(),wso.getZ(),r,phi,theta);
				moveCamera();
			}
		}
		
	}
	
	/** A mouse listener used to move the camera around in the world. **/
	private final class MouseListenerImpl implements MouseListener, MouseMotionListener, MouseWheelListener {

		// The position where the mouse was pressed in the component
		private Point from;
		
		// The pressed button
		private int button;
		
		/*============================================================
		Public Methods
		============================================================*/
		
		@Override public void mouseClicked(MouseEvent event) {};

		@Override public void mouseEntered(MouseEvent event) {};

		@Override public void mouseExited(MouseEvent event) {};

		@Override public void mousePressed(MouseEvent event) {
			requestFocus();
			if (from == null) {
				from = event.getPoint();
				button = event.getButton();
			}
		}

		@Override public void mouseReleased(MouseEvent event) {
			if (from != null) {
				final Point to = event.getPoint();
				switch (button) {
				case (MouseEvent.BUTTON1): {
					final double phi = WorldViewContainer.this.phi + spinFactor*(from.x - to.x);
					final double theta = WorldViewContainer.this.theta - spinFactor*(from.y - to.y);	
					setCamera(tx,ty,tz,r,phi,theta);
				} break;
				case (MouseEvent.BUTTON2): {
					final double k = moveFactor*WorldViewContainer.this.r;
					final double tx = WorldViewContainer.this.tx + k*(from.x - to.x);
					final double ty = WorldViewContainer.this.ty - k*(from.y - to.y);
					setCamera(tx,ty,tz,r,phi,theta);
				} break;
				case (MouseEvent.BUTTON3): {
					final double k = moveFactor*WorldViewContainer.this.r;
					final double tx = WorldViewContainer.this.tx + k*(from.x - to.x);
					final double tz = WorldViewContainer.this.tz + k*(from.y - to.y);
					setCamera(tx,ty,tz,r,phi,theta);
				}  break;
				}
				moveCamera();
				from = null;
			}
		}

		@Override public void mouseDragged(MouseEvent event) {
			if (from != null) {
				final Point to = event.getPoint();
				switch (button) {
				case (MouseEvent.BUTTON1): {
					final double phi = WorldViewContainer.this.phi + spinFactor*(from.x - to.x);
					final double theta = WorldViewContainer.this.theta - spinFactor*(from.y - to.y);
					moveCamera(tx,ty,tz,r,phi,theta);
				} break;
				case (MouseEvent.BUTTON2): {
					final double k = moveFactor*WorldViewContainer.this.r;
					final double tx = WorldViewContainer.this.tx + k*(from.x - to.x);
					final double ty = WorldViewContainer.this.ty - k*(from.y - to.y);
					moveCamera(tx,ty,tz,r,phi,theta);
				} break;
				case (MouseEvent.BUTTON3): {
					final double k = moveFactor*WorldViewContainer.this.r;
					final double tx = WorldViewContainer.this.tx + k*(from.x - to.x);
					final double tz = WorldViewContainer.this.tz + k*(from.y - to.y);
					moveCamera(tx,ty,tz,r,phi,theta);
				}  break;
				}
			}
		}

		@Override public void mouseMoved(MouseEvent event) {}

		@Override public void mouseWheelMoved(MouseWheelEvent event) {
			final double r = WorldViewContainer.this.r * (1 + zoomFactor*event.getWheelRotation());
			setCamera(tx,ty,tz,r,phi,theta);
			moveCamera();
		}
		
	}

}
